import os
import sys
import Image
import math


__linking_strength = 1
class Neuron:
    def __init__(self):
        self.x = 0
        self.y = 0
        self.feedings = []
        self.M_weights = []
        self.linkings = []
        self.W_weights = []
        self.thresholds = []
        self.outputs = []

    def feed(self, input, iteration, neighbour_inputs):
        # TODO: remove bullshit
        a_f = 0.01
        v_f = 1
        feeding = input
        if (iteration > 0):
            feeding += (math.exp(a_f) * self.feedings[iteration - 1] +
                    v_f * sum(map(lambda m, i: m * i, self.M_weights, neighbour_inputs)))
        if (iteration < len(self.feedings)):
            self.feedings[iteration] = feeding
        else:
            self.feedings.append(feeding)
        return feeding

    def link(self, iteration, neighbour_inputs):
        # TODO: remove bullshit
        a_l = 0.01
        v_l = 1
        linking = 0
        if (iteration > 0):
            linking += (math.exp(a_l) * self.linkings[iteration - 1] +
                    v_l * sum(map(lambda w, i: w * i, self.W_weights, neighbour_inputs)))
        if (iteration < len(self.linkings)):
            self.linkings[iteration] = linking
        else:
            self.linkings.append(linking)
        return linking

    def threshold(self, iteration):
        # TODO: remove bullshit
        a_t = 0.01
        v_t = 100
        threshold = v_t * self.outputs[iteration]
        if (iteration > 0):
            threshold += math.exp(a_t) * self.thresholds[iteration - 1]
        if (iteration < len(self.thresholds)):
            self.thresholds[iteration] = threshold
        else:
            self.thresholds.append(threshold)
        return threshold

    def output(self, iteration, U, O):
        output = 0
        if (U > O):
            output = 1
        if (iteration < len(self.outputs)):
            self.outputs[iteration] = output
        else:
            self.outputs.append(output)
        return output


    def fast_linking(self, iteration, neighbour_inputs, linking_strength, F, O, Y, neurons):
        fast_Y = Y
        L = 0
        # TODO: remove bullshit
        v_l = 1
        while (fast_Y == Y):
            L = v_l * sum(map(lambda w, i: w * i, self.W_weights, neighbour_inputs))
            U = F * (1 + linking_strength * L)
            O = 0.5 if (iteration == 0) else self.threshold(iteration - 1)
            print U, O
            fast_Y = self.output(iteration, U, O)
        return fast_Y

    def get_output(self, input, iteration, neighbour_inputs, linking_strength, nerons):
        F = self.feed(input, iteration, neighbour_inputs)               # feeding
        L = self.link(iteration, neighbour_inputs)                      # linking
        U = F * (1 + linking_strength * L)                              # internal state
        O = 0.5 if (iteration == 0) else self.threshold(iteration - 1)  # threshold to compare internal state to
        # compare internal state to threshold and decide what the output is
        Y = self.output(iteration, U, O)

        # fast linking
        #if (iteration > 0):
            #Y = self.fast_linking(iteration, neighbour_inputs, linking_strength, F, O, Y, neurons)

        return Y


class NeuralNetwork:
    def __init__(self):
        self.neurons = []
        self.convolution_kernel_radius = 0
        self.size = ()


    def init(self, im, convolution_kernel_radius):
        self.convolution_kernel_radius = convolution_kernel_radius
        self.size = im.size
        for x in range(im.size[0]):
            self.neurons.append([]);
            for y in range(im.size[1]):
                neuron = Neuron()
                neuron.x = x
                neuron.y = y
                num_of_neighbours = len(self.get_neighbours(neuron))
                # initial arrays of weights
                neuron.M_weights = [1 for a in range(num_of_neighbours)]
                neuron.W_weights = [1 for a in range(num_of_neighbours)]
                self.neurons[x].append(neuron)

    def get_neighbours(self, neuron):
        neighbours = []
        top = neuron.y - self.convolution_kernel_radius
        left = neuron.x - self.convolution_kernel_radius
        bottom = neuron.y + self.convolution_kernel_radius
        right = neuron.x + self.convolution_kernel_radius

        # check bounds
        def check_bounds(n, axis):
            if (n < 0): n = 0
            if (axis == 'x'):
                if (n >= self.size[0]): n = self.size[0] - 1
            elif (axis == 'y'):
                if (n >= self.size[1]): n = self.size[1] - 1
            return n
        top = check_bounds(top, 'y')
        left = check_bounds(left, 'x')
        bottom = check_bounds(bottom, 'y')
        right = check_bounds(right, 'x')

        # add neighbour coordinates to list
        for x in range(left, right + 1):
            for y in range(top, bottom + 1):
                if (x != neuron.x or y != neuron.y):
                    neighbours.append((x, y))
        return neighbours


    def pulse(self, input, iteration):
        for neuron_row in self.neurons:
            for neuron in neuron_row:
                neighbour_signals = []
                if (iteration > 0):
                    # get neighbour signals
                    for neighbour in [self.neurons[xy[0]][xy[1]] for xy in self.get_neighbours(neuron)]:
                        neighbour_signals.append(neighbour.outputs[iteration - 1])

                neuron.get_output(input[neuron.x][neuron.y], iteration, neighbour_signals, 1, self.neurons)
        print "iteration %d" % iteration


def main():
    #IMAGE = "abstract"
    #IMAGE = "blah"
    IMAGE = "car"
    #IMAGE = "face"

    im = Image.open("{0}.jpg".format(IMAGE))
    print im.format, im.size, im.mode

    #init network with image
    nn = NeuralNetwork()
    nn.init(im, 1)
    print "init done"

    # TESTS
    pix = im.load()
    input = []
    for x in range(im.size[0]):
        input.append([])
        for y in range(im.size[1]):
            # TODO: right now R is taken from RGB
            input[x].append((255 - pix[x, y][0]) / 255.0)   # dark pixel is positive impulse
            #input[x].append(pix[x, y][0] / 255.0)           # bright pixel is positive impulse



    def get_pulse_output(neural_network, iteration):
        for neuron_row in neural_network.neurons:
            for neuron in neuron_row:
                pulse_val = (1 - neuron.outputs[iteration]) * 255 # positive output is a black pixel
                pix[neuron.x, neuron.y] = (pulse_val, pulse_val, pulse_val)
        im.save("test{0}.jpg".format(iteration))

    for i in range(8):
        nn.pulse(input, i)
        get_pulse_output(nn, i)



if __name__ == "__main__":
    main()
